Explore Concurrency in Swift in this in-depth article.
Concurrency:
- The ability to execute multiple at the same time (concurrently).
- It enables responsiveness of system resources and better performance.
Concurrent execution has 2 types:
-
Concurrent, non-parallel execution (also known as Concurrency).
- A property of a program, is more about software design.
- Achieved through interleaving operation / context switching.
- Needs just one core.
- It's all about managing multiple tasks (start, run, and complete):
- At the same time (in overlapping time periods).
- In no specific order.
- Abstracted from hardware details.
- When a task is divided into multiple parts and we quickly switch from one task/part to another, so it seems that all the tasks run at the same time, it produces illusion of parallelism.
-
Concurrent, parallel execution (also known as Parallelism).
- A property of a machine (multiple CPUs), is more about hardware.
- Achieved through using multiple CPUs.
- Needs at least 2 cores.
- It's all about executing multiple tasks:
- At the same time.
- By multiple threads.
- When a task is divided into multiple parts and we literally run two or more tasks/parts at the exactly same time, e.g., on a multicore processor.
- Task - an abstract concept of work that needs to be performed.
- Process - a running executable, which can encompass multiple threads.
- iOS does not support multiple processes for one app. You only have one process.
- macOS supports multiple processes for one app.
- Thread - a separate path of execution for code.
- Compared to processes, threads share their memory with their parent process.
- Threads are a limited resource on iOS - there are 64 threads at the same time for one process.
- The initial thread – the one the app is first launched with.
- Always exists for the lifetime of the app.
- User interface work must take place on the main thread:
- When you try to update your UI from any other thread:
- Nothing happens.
- The app crashes.
- Or pretty much anywhere in between.
- When you try to update your UI from any other thread:
- Run Loop is a mechanism that allows threads to process events at any time without exiting.
- It is a loop tied to a single thread.
- Example:
- There is an incoming event on the thread.
- The thread enters the run loop (it creates one if needed).
- Thread uses the run loop tot run event handlers in response to incoming events.
- Implementations:
CFRunLoopRef- Class from CoreFoundation framework.
- Provides APIs for pure C functions, all of which are thread-safe.
RunLoop- Wrapper based on
CFRunLoopRef - Provides object-oriented APIs, but these APIs are not thread-safe.
- Wrapper based on
- A run loop receives events from two different types of sources:
- Input sources: they deliver asynchronous events to your threads.
- Port-Based Sources.
- Custom Input Sources.
- Cocoa Perform Selector Sources (
performSelectorOnMainThread).
- Timer sources: they deliver synchronous events, at a scheduled time or repeating interval.
TimerorCFRunLoopTimerclasses.
- Input sources: they deliver asynchronous events to your threads.
- Time Slicing - is the process of allocating time to threads by the CPU.
- Context Switching - is the process of:
- Storing the state of a process or thread, so that it can be restored and resume execution at a later point.
- Then restoring a different, previously saved, state.
Swift provides different tools to use Concurrency:
- Manual thread creation.
- Grand Central Dispatch (GCD).
- Operation Queues.
- Modern Swift Concurrency.
- Swift provide
Threadclass to run the code in its own thread of execution.- You can start, sleep, terminate created thread, check its state.
- Disadvantage: the manual thread management is complex:
- Developer responsibilities - the burden of creating a scalable solution:
- Select the optimal number of threads for an application.
- Keep the optimal number of threads based on current system load and the underlying hardware.
- Synchronize threads without losing performance and application correctness.
- Application responsibilities:
- Most of the costs associated with creating and maintaining any threads it uses.
- Each thread you create needs to run somewhere:
- If you accidentally end up creating 40 threads when you have only 4 CPU cores, the system will need to spend a lot of time just swapping them.
- Risks:
- Thread Explosion - when you create many more threads compared to the number of available CPU cores.
- Developer responsibilities - the burden of creating a scalable solution:
- There are other solutions that move thread management code to the system level.
Grand Central Dispatch (GCD):
- It's a low-level API, built on top of threads, for managing concurrency in Swift using the concept of dispatch queues.
- It offers easier concurrency model than the manual thread management.
- It gives you automatic thread pool management (creating threads, scheduling, optimizing their usage).
- You don't care how some code runs on the CPU.
- You only care about the task and how to execute it:
- Defining the task you want to run.
- Deciding which queue to use (The Main Queue, The Global Queue, Custom Queues).
- Deciding on the execution order of a task: serially or concurrently.
- Deciding on how to schedule the task: synchronously or asynchronously.
- It uses Dispatch Queues:
DispatchQueueclass.- Dispatch queues are thread-safe (you can simultaneously access them from multiple threads).
- Types:
- The Main Queue (serial).
- Global Queues (concurrent).
- Custom Queues (serial and concurrent).
- Dispatch queue types - how many tasks are executed at a time and in what order?
- Serial queues:
- How many tasks are executed at a time?
- Executing one task at a time:
- The task must finish before starting new task in the queue (the risk of a deadlock).
- Executing one task at a time:
- The order of execution:
- Tasks executed in the same order as it was added to the queue (FIFO).
- Example: The Main Queue.
- How many tasks are executed at a time?
- Concurrent queues:
- How many tasks are executed at a time?
- Executing multiple tasks at a time.
- The order of execution:
- Tasks executed concurrently within this queue in random order.
- Example: Global Concurrent Queues.
- How many tasks are executed at a time?
- Serial queues:
- Types of task execution in GCD (how can a task be dispatched to a queue):
- Synchronously
- Blocks the current execution (the dispatching thread waits).
- Waits until the task is completed.
- Then it continues further.
- Asynchronously
- Does not block the current execution (the dispatching thread continues execution).
- Does not wait for the task to complete.
- Returns immediately and continues further.
- Synchronously
- Task cancellation in GCD:
- It's not directly supported.
- However, you can check for a cancellation flag within the task code or using
DispatchWorkItem'scancel()method.
- It's a high-level API, built on top of GCD, for managing concurrency in Swift using the concept of operation and operation queue.
- Instead of a blocks (GCD), you work with operations.
- You can control state, priority and dependencies of operations.
- Creating an operation can also be done in multiple ways:
- By creating an
BlockOperationorNSInvocationOperation(only in Objective C) - By subclassing an abstract class
Operation.
- By creating an
- Danger of memory leak:
- Operation queues retain operations until they're finished.
- Operation queues are retained until all operations are finished.
- As a result, suspending an operation queue with operations that aren't finished can result in a memory leak.
- Operation Queues vs GCD:
- Object-oriented API.
- Dependencies support
- However, GCD supports barrier flag which might result in similar behaviour).
- Operations can be paused, resumed, and cancelled.
- However, GCD support
DispatchWorkItemcan be cancelled.
- However, GCD support
- There are different states of an operation, depending on its current execution status:
- Ready: It's ready to execute
- Operation become ready to execute when all of its dependent operations have finished executing.
- Executing: The task is currently running.
- Finished: Once the process is completed.
- Cancelled: The task cancelled.
- You can check the operation state:
isReady,isExecuting,isFinished,isCancelled.
- Ready: It's ready to execute
OperationandOperationQueueclasses have a number of properties that can be observed, using KVO.- You can define operation dependencies.
- You can define operation
queuePriorityandqualityOfService:- If all of the queued operations have the same
queuePriorityand are ready to execute when they are put in the queue, they're executed in the order in which they were submitted to the queue. - Otherwise, the operation queue always executes the one with the highest priority relative to the other ready operations.
- If all of the queued operations have the same
- An operation can only execute once: you can't restart the same instance.
- Operation cancellation:
- It's supported by calling the
cancel()method on an Operation object. - It's important to handle cancellation appropriately within the task code and check for the cancellation flag regularly.
- Canceling an operation causes the operation to ignore any dependencies it may have. This behavior makes it possible for the queue to invoke the operation’s start() method as soon as possible. The start() method, in turn, moves the operation to the finished state so that it can be removed from the queue.
- It's supported by calling the
- Operation lifecycle in the operation queue:
- The given task gets added to the
OperationQueuethat will start the execution as soon as possible. - The
OperationQueuewill remove the task automatically from its queue once it becomes finished or cancelled.
- The given task gets added to the
- You can define
qualityOfService. - You can define
maxConcurrentOperationCount. - When adding an operation you can define synchronous/asynchronous execution using
waitUntilFinished.operationQueue.addOperations([op], waitUntilFinished: false).
- You can
cancelAllOperations(). - You can
waitUntilAllOperationsAreFinished().
// 1. Custom subclass of Operation
final class ImportOperation: Operation {
let repositoryId: String
init(repositoryId: String) {
self.repositoryId = repositoryId
super.init()
}
override func main() {
guard !isCancelled else {
return
}
print("Importing repository..")
}
}
// 2. BlockOperation
let operation1 = BlockOperation {
print("BlockOperation")
}
// You can define completion block
operation1.completionBlock = {
print("BlockOperation completed")
}
let operation2 = ImportOperation(repositoryId: "xyz")
let operationQueue: OperationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2
operationQueue.addOperations([operation1], waitUntilFinished: false)
operation2.addDependency(operation1) // execute operation1 before operation2Read more about Swift Concurrency here.
- What is the difference between concurrency and parallelism?
- Parallel programming with Swift: Basics
- Run Loops - Threading Programming Guide
- About Threaded Programming - Threading Programming Guide
- Migrating Away from Threads - Threading Programming Guide
- What is the difference between concurrency, parallelism and asynchronous methods? - Stack Overflow
- Concurrency vs Parallelism: 2 sides of same Coin?
- Understanding threads and queues
- Getting started with Operations and OperationQueues in Swift
- Parallel Programming with Swift — Part 3/4
- OperationQueue
